import DraggableCore from "./DraggableCore";
import { DraggableData, createDraggableData, canDragX, canDragY, createCSSTransform, getBoundPosition, ControlPosition } from './utils';
import { PropType, computed, defineComponent, onUnmounted, reactive } from "vue";

type DraggableProps = {
    defaultPosition?: any,
    position?: any,
    scale?: number,
    bounds?: string,
    axis?: 'x'|'y'|'both',
    positionOffset?: any,
    grid?: number[],
    disabled?: boolean,
    handle?: string|HTMLElement,
    onStart?: (e: any, data: DraggableData) => boolean | undefined,
    onDrag?: (e: any, data: DraggableData) => boolean | undefined,
    onStop?: (e: any, data: DraggableData) => boolean | undefined,
}

export default defineComponent({
    name: 'Draggable',
    props: {
        defaultPosition: {type: Object as PropType<DraggableProps['defaultPosition']>},
        position: {type: Object as PropType<DraggableProps['position']>},
        scale: {type: Number as PropType<DraggableProps['scale']>},
        bounds: {type: String as PropType<DraggableProps['bounds']>},
        axis: {type: String as PropType<DraggableProps['axis']>},
        positionOffset: {type: Object as PropType<DraggableProps['positionOffset']>},
        grid: {type: Array as PropType<DraggableProps['grid']>},
        disabled: {type: Boolean as PropType<DraggableProps['disabled']>},
        handle: {type: [String, Object] as PropType<DraggableProps['handle']>},
        onStart: {type: Function as PropType<DraggableProps['onStart']>},
        onDrag: {type: Function as PropType<DraggableProps['onDrag']>},
        onStop: {type: Function as PropType<DraggableProps['onStop']>},
    },
    setup (props, {expose, slots}) {
        const defaultPosition = props.defaultPosition || {x: 0, y: 0};
        const store = reactive({
            // Whether or not we are currently dragging.
            dragging: false,

            // Whether or not we have been dragged before.
            dragged: false,

            // Current transform x and y.
            x: props.position ? props.position.x : defaultPosition.x,
            y: props.position ? props.position.y : defaultPosition.y,

            prevPropsPosition: {...props.position},

            // Used for compensating for out-of-bounds drags
            slackX: 0, slackY: 0
        });

        const scale = props.scale || 1;
        const bounds = props.bounds || false;
        let core: any;
        const onDragStart = (e: any, coreData: DraggableData) => {
            // Short-circuit if user's callback killed it.
            const shouldStart = props.onStart && props.onStart(e, createDraggableData(store, scale, coreData));
            // Kills start event on core as well, so move handlers are never bound.
            if (shouldStart === false) return false;

            store.dragging = true;
            store.dragged = true;
        };

        const onDrag = (e: any, coreData: DraggableData) => {
            if (!store.dragging) return false;
            const uiData = createDraggableData(store, scale, coreData);

            const newState = {
                x: uiData.x,
                y: uiData.y,
                slackX: 0,
                slackY: 0
            };

            // Keep within bounds.
            if (bounds) {
                // Save original x and y.
                const {x, y} = newState;

                // Add slack to the values used to calculate bound position. This will ensure that if
                // we start removing slack, the element won't react to it right away until it's been
                // completely removed.
                newState.x += store.slackX;
                newState.y += store.slackY;

                // Get bound position. This will ceil/floor the x and y within the boundaries.
                const [newStateX, newStateY] = getBoundPosition({bounds, node: coreData.node}, newState.x, newState.y);
                newState.x = newStateX;
                newState.y = newStateY;

                // Recalculate slack by noting how much was shaved by the boundPosition handler.
                newState.slackX = store.slackX + (x - newState.x);
                newState.slackY = store.slackY + (y - newState.y);

                // Update the event we fire to reflect what really happened after bounds took effect.
                uiData.x = newState.x;
                uiData.y = newState.y;
                uiData.deltaX = newState.x - store.x;
                uiData.deltaY = newState.y - store.y;
            }

            // Short-circuit if user's callback killed it.
            const shouldUpdate = props.onDrag && props.onDrag(e, uiData);
            if (shouldUpdate === false) return false;

            store.x = newState.x;
            store.y = newState.y;
            store.slackX = newState.slackX;
            store.slackY = newState.slackY;
        };

        const onDragStop = (e: any, coreData: DraggableData) => {
            if (!store.dragging) return false;

            // Short-circuit if user's callback killed it.
            const shouldContinue = props.onStop && props.onStop(e, createDraggableData(store, scale, coreData));
            if (shouldContinue === false) return false;

            store.dragging = false;
            store.slackX = 0;
            store.slackY = 0;
        };
        onUnmounted(() => {
            store.dragging = false;
        });

        const axis = props.axis || 'both';


        const style = computed(() => {
            const x = store.x;
            const y = store.y;
            const transformOpts = () => ({
                // Set left if horizontal drag is enabled
                x: canDragX(axis) ? x : defaultPosition.x,
                // Set top if vertical drag is enabled
                y: canDragY(axis) ? y : defaultPosition.y
            });

            return {...createCSSTransform(transformOpts(), props.positionOffset)};
        });
        const classList = computed(() => ({
            'cm-draggable': true,
            'cm-draggable-dragging': store.dragging,
            'cm-draggable-dragged': store.dragged,
        }));

        // 暴露接口
        expose({
            reset: () => {
                store.x = 0;
                store.y = 0;
            },
            setPosition (pos: ControlPosition) {
                store.x = pos.x;
                store.y = pos.y;
            }
        });

        return () => <DraggableCore grid={props.grid} class={classList.value} disabled={props.disabled} handle={props.handle} scale={scale} style={style.value}
            onStart={onDragStart} onDrag={onDrag} onStop={onDragStop} ref={(el) => core = el}>
            {slots.default?.()}
        </DraggableCore>;
    }
});
